package com.lhl.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.connection.jedis.JedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * @author LHL
 * @date: 2019/3/9
 */
@RestController
@RequestMapping(value = "/redis")
public class RedisController {


    private static final Logger LOGGER = LoggerFactory.getLogger(RedisController.class);
    public static final String UNLOCK_LUA;
    private ThreadLocal<String> lockFlag = new ThreadLocal<>();

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }

    //总库存
    private long product_count = 0;
    int timeout = 999;
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping(value = "/demo1")
    public String redisDemo() {
        User user = new User();
        user.setId(111L);
        user.setName("eastsoft");
        redisTemplate.opsForValue().set("es", user);
        redisTemplate.opsForValue().set("es1", "stringdemo");
        redisTemplate.opsForZSet().add("order", 1, 100);
        redisTemplate.opsForZSet().add("order", 3, 300);
        redisTemplate.opsForZSet().add("order", 2, 200);
        System.out.println("============================");
        Set<ZSetOperations.TypedTuple<Long>> order = redisTemplate.opsForZSet().reverseRangeWithScores("order", 0, 1);
        while (order.iterator().hasNext()) {
            ZSetOperations.TypedTuple<Long> next = order.iterator().next();
            Double score = next.getScore();
            Long value = next.getValue();
            System.out.println(score + "----" + value);
        }
        System.out.println(order);
        return "{}";
    }

    @GetMapping(value = "demo2")
    public List<String> redisLockDemo() {

        //抢到商品的用户
        List<String> shopUsers = new ArrayList<>();
        //构造很多用户
        List<String> users = IntStream.range(0, 100).parallel().mapToObj(b -> "ID-" + b).collect(Collectors.toList());
        String key = "product_id";
        //初始化库存
        product_count = 10;
        System.out.println("=========================");
        users.parallelStream().forEach(user -> {
            String shopUser = buy(key, user);
            if (!StringUtils.isEmpty(shopUser)) {
                shopUsers.add(shopUser);
            }
        });
        return shopUsers;

    }

    private String buy(String key, String value) {
        //用户开抢时间
        long startTime = System.currentTimeMillis();

        //未抢到的情况下，30秒内继续获取锁
        while ((startTime + timeout) >= System.currentTimeMillis()) {
            //商品是否剩余
            if (product_count <= 0) {
                LOGGER.info("用户{}，库存为0",value);
                break;
            }
            if (setNx(key, value, 4)) {
                //用户b拿到锁
                LOGGER.info("=====用戶{}获得锁",value);
                try {
                    //商品是否剩余
                    if (product_count <= 0) {
                        break;
                    }
                    //模拟生成订单耗时
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //抢购成功，商品递减，记录用户
                    product_count -= 1;
                    //抢单成功跳出
                    LOGGER.info("用户{}抢单成功,所剩库存：{}", value, product_count);
                    return value + "抢单成功，所剩库存：" + product_count;
                } finally {
                    boolean delkey = delkey(key);
                    LOGGER.info("用户{}释放锁{}...", value,delkey);
                    //释放锁  redisTemplate.delete(key);错误发方式
                    //此处为什么不能直接删除对应的key， 原因：如果获得到锁，程序的执行时间超过了锁的过期时间，那么别的线程在来访问的时候
                    //就可以获得到锁（锁已过期，自动被删除）,如果此时直接删除锁，则此时删除的锁是错误的，并不是当前线程持有的锁
                }
            }else {
                LOGGER.info("用户{}未获取锁，重试获取",value);
                //1秒之后再尝试获取
//                try {
//                    Thread.sleep(1*1000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
            }
        }
        return "";
    }

    public boolean setNx(String lockKey, String requestId, int expireTime) {
        boolean success = (boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                lockFlag.set(UUID.randomUUID().toString());
                return connection.set(lockKey.getBytes(), lockFlag.get().getBytes(), Expiration.from(expireTime, TimeUnit.SECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
            }
        });
        return success;
    }

    public boolean delkey(String key) {
        boolean execute = (boolean) redisTemplate.execute(new RedisCallback() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN, 1, key.getBytes(), lockFlag.get().getBytes());
            }
        });
        return execute;
    }


}
